/*
 *  Copyright (C) 1993  Robert Davis
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of Version 2, or any later version, of 
 *  the GNU General Public License as published by the Free Software 
 *  Foundation.
 */

static char RCSId[]="$Id: FunctionObject.m,v 1.12 1993/05/24 03:59:44 davis Exp $";


#import <streams/streams.h>
#import <sys/stat.h>		/* stat()			*/
#import <sys/types.h>		/* stat()			*/
#import <objc/hashtable.h>	/* NXCopyStringBufferFromZone()	*/
#import <objc/zone.h>
#import <ctype.h>		/* isspace()			*/
#import <libc.h>		/* index(), rindex()		*/

#import "FunctionObject.h"


const char *styleString[] = {"lines", "points", "linespoints", "dots",
			     "impulses", "errorbars", "boxes", "boxerrorbars",
			     "steps"};


/*  Is the string blank (i.e. full of only white space)?  */
static BOOL _isBlank (const char *aString)
{
    const char *cur;

    for (cur = aString ; *cur != '\0' ; cur++)
	if (!isspace(*cur))
	    return NO;

    return YES;
}


static BOOL _isSpaceNotNL (char aChar)
{
    return (isspace (aChar)) && (aChar != '\n');
}




/*  
 *  aString is assumed to be a one-line string (i.e it contains no 
 *  newlines).  This could be changed easily if need be.
 */
static void _removeComments (char *aString)
{
    char *cur = aString;

    while (*cur) {
	if ((*cur == '\'') || (*cur == '"')) {

	    /* 
	     *  Skip the quotation.  Note that gnuplot does not 
	     *  support nested quotes.
	     */
	    
	    char c = *(cur++);
	    while (*cur && (*cur != c))
		cur++;

	} else if (*cur == '#') {

	    /*  
	     *  If we've encountered a pound sign outside of a 
	     *  quotation, we've found a comment.  The rest of the 
	     *  line can be truncated.
	     */
	    cur = '\0';

	}

	cur++;
    }

}


static void _getWith (char *aString, int *style, int *lineStyle,
		      int *pointsStyle)
{
    char *cur, *with;

    /* 
     *  This attempts to detect a 'with' phrase and to extract the 
     *  style from it if it exists.  There are several ways to trick 
     *  this. 
     */
    if ((with = rindex (aString, 'w')) && (*(with+1) == 'i') &&
	(cur = index (with, ' '))) {
	switch (*(++cur)) {
	case 'p':  *style = FUNCTION_POINTS;	break;
	case 'd':  *style = FUNCTION_DOTS;	break;
	case 'i':  *style = FUNCTION_IMPULSES;	break;
	case 'e':  *style = FUNCTION_ERRORBARS;	break;
	case 'b':
	    if (index (cur, 'r')) *style = FUNCTION_BOXERRORBARS;
	    else *style = FUNCTION_BOXES;
	    break;
	case 'l':
	    if (index (cur, 'p')) *style = FUNCTION_LINESPOINTS;
	    else *style = FUNCTION_LINES;
	    break;
	case 's':  *style = FUNCTION_STEPS;	break;
	default:   *style = FUNCTION_NOSTYLE;	break;
	}
	*(with-1) = '\0';	/* Assume with is not first in aString */

	/* 
	 *  If the style type includes lines or points, the user has 
	 *  the option of setting the point/line styles.  Check for 
	 *  those...
	 */
						/* Line Style */
	if ((*style == FUNCTION_POINTS) || (*style == FUNCTION_LINES) ||
	    (*style == FUNCTION_LINESPOINTS)) {

	    while (cur && *cur && ((*cur > '6') || (*cur < '1')))
		cur++;
	    if (cur && *cur)
		*lineStyle = *cur - '0';

						/* Point Style */
	    if ((*style == FUNCTION_POINTS) ||
		(*style == FUNCTION_LINESPOINTS))  {

		while (cur && *cur && ((*cur > '6') || (*cur < '1')))
		    cur++;
		if (cur && *cur)
		    *pointsStyle = *cur - '0';
	    }

	}

    }

}




static char *_getTitle (char *aString)
{
    while (aString && *aString) {

	/*
	 *  If this appears to be a "title" phrase, see if a quoted
	 *  string follows.  If one does, chop it off and return it.
	 */
	
	if ((*aString == 't') &&
	    ((*(aString + 1) == 'i') || _isSpaceNotNL (*(aString + 1)))) {

	    char *firstQuote, *beginning = aString;

	    while (!_isSpaceNotNL (*aString))
		aString++;
	    while (_isSpaceNotNL (*aString))
		aString++;
	    if ((*aString == '\'') || (*aString == '"')) {

		/*  
		 *  Okay, we are as sure as can be that this is a 
		 *  "title" phrase.
		 */
		firstQuote = aString;
		if (aString = index (++aString, *firstQuote))
		    *aString = '\0';
		*(beginning-1) = '\0';	/* Assume title isn't first in aString */

		return firstQuote+1;
	    }

	}

	aString++;
    }

    return NULL;
}


/* aString assumed to contain no newlines */
static void _getUsing (char *aString, int *nums, BOOL threeD)
{
    while (aString && *aString) {


	/*  Look for a using clause  */
	
	if ((*aString == 'u') && (*(aString + 1)) &&
	    ((*(aString + 1) == 's') || isspace (*(aString + 1)))) {

	    int		i = 0;

	    *aString = '\0';
	    /* skip "using" and space */
	    while (*(++aString) && !isspace (*aString))
		;
	    while (*(++aString) && isspace (*aString))
		;

	    while (*aString) {
		if (isdigit (*aString))
		    nums[i] = (nums[i] * 10) + (*aString - '0');
		else if (nums[i] == '0') {
		    /* Stop reading -- problems */
		    *(aString+1) = '\0';
		    i--;
		} else if (*aString == ':') {
		    if (i++ > 4) {
			/* Read all the numbers -- ignore anything else */
			*(aString+1) = '\0';
			i--;
		    }
		}

		aString++;
	    }

	    return;

	} else if (*aString == '/') {
	    while (*(++aString) && !isspace(*aString))
		;
	} else if ((*aString == '"') || (*aString == '\'')) {
	    char quote = *aString;
	    while (*(++aString) && (*aString != quote))
		;
	    aString++;
	} else
	    aString++;
    }

    return;
}


static BOOL _isNumberElement (char c)
{
    return isdigit(c) || (c == '.') || (c == 'E') || (c == '+') || (c == '-');
}

static int _getNumColumns (const char *path)
{
    NXStream	*s = NXMapFile (path, NX_READONLY);
    int		count = 0;

    if (s) {
	int		c, oldc = '\0';

	c = NXGetc(s);
	while (c != EOF)  {
	    if (c == '#') {
		if (count == 0) {
		    while (((c = NXGetc(s)) != EOF) && (c != '\n'))
			;
		    count = 0;
		    oldc = c;
		    if (c != EOF)
			c = NXGetc(s);
		} else
		    c = EOF;
	    } else if (_isNumberElement(c) && !_isNumberElement(oldc)) {
		count++;
		oldc = c;
		c = NXGetc(s);
	    } else if (c == '\n') {
		if (count > 0)
		    c = EOF;
		else
		    c = NXGetc(s);
	    } else {
		oldc = c;
		c = NXGetc(s);
	    }

	}

	NXCloseMemory (s, NX_FREEBUFFER);
    }

    return count;
}



@interface FunctionObject (Private)
- _initColumnDataUsing:(int *)usingData;
@end


@implementation FunctionObject


/*  Does the string contain something besides white space?  */
+ (BOOL) isAcceptableStringValue:(const char *)aString
{
    return (aString && !_isBlank (aString));
    /* todo, also check for valid functions, if possible */
}
	

/*  
 *  Makes sure the file whose full path is specified by aString is a 
 *  regular, readable file.
 */
+ (BOOL) isAcceptableDataFile:(const char *)aString
{
    struct stat fileinfo;
    BOOL returnVal;

    returnVal = (aString && !stat (aString, &fileinfo) &&
		 ((fileinfo.st_mode & S_IFMT) == S_IFREG) &&
		 (fileinfo.st_mode & S_IREAD));
    return returnVal;
}



/*  
 *  The attributes of a function are stored by an instance of 
 *  FunctionObject.  The only complicated part is parsing those 
 *  attributes from a Gnuplot "plot" statement.  aString should 
 *  specify only the part of that statement pertaining to the function 
 *  that will be stored by this instance and should contain no leading 
 *  white space.  (This method could be nicer by ignoring leading 
 *  white space.)
 */
- initFromString:(const char *)aString isThreeD:(BOOL)aCond
{
    char	quote, *cur;
    NXZone	*zone;
    int		theUsing[5] = {0,0,0,0,0};

    [super init];

    zone = [self zone];

    title = NULL;			/* Defaults */
    style = FUNCTION_NOSTYLE;
    pointsStyle = POINTS_NOSTYLE;
    lineStyle = LINE_NOSTYLE;
    isDataFile = NO;
    isThreeD = aCond;

    if (aString)  {

	stringValue = NXCopyStringBufferFromZone (aString, zone);
	_removeComments (stringValue);
	_getWith (stringValue, &style, &lineStyle, &pointsStyle);
	[self setTitle:_getTitle (stringValue)];

	/* 
	 *  If this string value is quoted or if it begins with a 
	 *  forward slash, it is a data file.  (This forward slash 
	 *  thing is an feature not present in the original gnuplot, 
	 *  but it's very nice in the NeXTSTEP interface.)  We remove
	 *  the quotes and set the isDataFile flag.  The string is 
	 *  assumed to have no leading white space, i.e. if it is a 
	 *  data file, the first character is a quote.
	 */

	quote = *stringValue;

	/*  Check to see if this is a data file  */
	if ((quote == '/') || (quote == '\'') || (quote == '"')) { /* It is */

	    isDataFile = YES;
	    _getUsing (stringValue, theUsing, isThreeD);

	    if (quote == '/') {
		int len;
		cur = NXCopyStringBufferFromZone (stringValue, zone);
		len = strlen (cur);	/* Remove trailing space */
		while (isspace(cur[--len]))
		    cur[len] = '\0';
	    } else {
		*rindex (stringValue, quote) = '\0';
		cur = NXCopyStringBufferFromZone (stringValue+1, zone);
	    }

	    NXZoneFree (zone, stringValue);
	    stringValue = cur;

	    [self _initColumnDataUsing:theUsing];

	} else {		/* This is a function, not a data file. */

	    /* Remove trailing blanks */
	    cur = stringValue + strlen (stringValue) - 1 ;
	    while (isspace (*cur) && (cur >= stringValue))
		*(cur--) = '\0';

	}

    } else
	stringValue = NULL;
      
  return self;
}
 
 
- free
{
    NXZoneFree ([self zone], title);
    return [super free];
}



- setThreeD:(BOOL)cond
{
    if (cond != isThreeD) {
	isThreeD = cond;

	if (isDataFile) {
	    struct coldat temp = columnData;
	    columnData = columnDataOther;
	    columnDataOther = temp;
	}
    }

    return self;
}


- (BOOL)isDataFile
{
    return isDataFile;
}


- (struct coldat *)columnData
{
    return isDataFile? &columnData :NULL;
}


- setTitle:(const char *)aString
{
    NXZone	*zone = [self zone];

    NXZoneFree (zone, title);
    if (aString) {
	char	*c, quote = '\0';

	title = NXCopyStringBufferFromZone (aString, zone);

	/* 
	 *  Make sure aString doesn't mix both kinds of quotation marks.  
	 *  Gnuplot can handle one or the other but not both in one title. 
	 *  (If there's a mix, use the kind used first.)
	 */
	
	for (c = title; c && *c; c++)
	    if ((*c == '\'') || (*c == '"')) {
		if (quote && (*c != quote))
		    *c = quote;
		else
		    quote = *c;
	    }
    
    } else
	title = NULL;

    return self;
}


- (const char *)title
{
    return title;
}


- setStyle:(int) anInt
{
    style = anInt;
    return self;
}


- (int)style
{
    return style;
}


- (const char *)styleString
{
    if (style == FUNCTION_NOSTYLE)
	return NULL;
    else
	return styleString[style];
}


- setPointsStyle: (int)anInt
{
    pointsStyle = anInt;
    return self;
}


- (int)pointsStyle
{
    return pointsStyle;
}


- setLineStyle: (int)anInt
{
    lineStyle = anInt;
    return self;
}


- (int)lineStyle
{
    return lineStyle;
}



/*  
 *  If the function is actually a data file, it is similar to an 
 *  attachment, or a "file link," so we return YES.  If the function 
 *  is not a data file, we return NO.
 */
- (BOOL)isAttachment
{
    return isDataFile;
}


// Shuts up the compiler about unused RCSId
- (const char *) rcsid
{
    return RCSId;
}


@end


@implementation FunctionObject (Private)

- _initColumnDataUsing:(int *)usingData
{
    struct coldat	*two = isThreeD? &columnDataOther : &columnData;
    struct coldat	*three = isThreeD? &columnData : &columnDataOther;
    int		numUsing;
    int		x;

    /*  
     *  Initialize the column data structures based on the number of 
     *  columns in the data file, but also consider the usingData.
     */
    
    two->number = three->number = _getNumColumns (stringValue);

    for (numUsing = 0; usingData[numUsing]; numUsing++)
	;

    two->isOn = three->isOn = numUsing;
    x = (numUsing)? numUsing : two->number;

    if (x >= 1) {
	two->useX = two->useY = three->useZ = YES;
	two->x = two->y = three->useZ = 1;
	if (x >= 2) {
	    two->y = 2;
	    if (x >= 3) {
		two->useYDelta = three->useX = three->useY = YES;
		two->yDelta = three->z = 3;
		three->y = 2;
		three->x = 1;
		if (x >= 4) {
		    two->useYLow = two->useYHigh = YES;
		    two->useYDelta = NO;
		    two->yLow = 3;
		    two->yHigh = 4;
		    if (x >= 5)
			two->useBoxWidth = YES;
			two->boxWidth = 5;
		}
	    }
	}
    }

    if (numUsing) {
	two->x = three->x = (numUsing > 1)? usingData[0] : 1;
	two->y = (numUsing > 1)? usingData[1] : usingData[0];
	three->y = usingData[1];
	two->yDelta = two->yLow = three->z = usingData[2];
	two->yHigh = usingData[3];
	two->boxWidth = usingData[4];
    }

    three->useYDelta = NO;	/* Can't use these in 3D */
    three->useYLow = NO;
    three->useYHigh = NO;
    three->useZ = YES;
    three->useBoxWidth = NO;

    two->useZ = NO;		/* Can't use this in 2D */

    switch (two->number) {	/* Note:  this falls through */
    case 0:	two->useY = three->useZ = NO;
    case 1:	two->useX = NO;
    case 2:	two->useYDelta = three->useX = three->useY = NO;
    case 3:	two->useYLow = two->useYHigh = NO;
    case 4:	two->useBoxWidth = NO;
    }

    return self;
}


@end
